var DdsUtils = require('../../loaders/dds/DdsUtils');
var Capabilities = require('../../renderer/Capabilities');

function DdsPixelFormat() {
	this.dwSize = 0;
	this.dwFlags = 0;
	this.dwFourCC = 0;
	this.dwRGBBitCount = 0;
	this.dwRBitMask = 0;
	this.dwGBitMask = 0;
	this.dwBBitMask = 0;
	this.dwABitMask = 0;
}

DdsPixelFormat.HEADER_OFFSET = 19;

// ---- VALUES USED IN dwFlags ----
// Texture contains alpha data; dwABitMask contains valid data.
DdsPixelFormat.DDPF_ALPHAPIXELS = 0x1;
// Used in some older DDS files for alpha channel only uncompressed data (dwRGBBitCount contains the alpha channel
// bitcount; dwABitMask contains valid data)
DdsPixelFormat.DDPF_ALPHA = 0x2;
// Texture contains compressed RGB data; dwFourCC contains valid data.
DdsPixelFormat.DDPF_FOURCC = 0x4;
// Texture contains uncompressed RGB data; dwRGBBitCount and the RGB masks (dwRBitMask, dwGBitMask, dwBBitMask)
// contain valid data.
DdsPixelFormat.DDPF_RGB = 0x40;
// Used in some older DDS files for YUV uncompressed data (dwRGBBitCount contains the YUV bit count; dwRBitMask
// contains the Y mask, dwGBitMask contains the U mask, dwBBitMask contains the V mask)
DdsPixelFormat.DDPF_YUV = 0x200;
// Used in some older DDS files for single channel color uncompressed data (dwRGBBitCount contains the luminance
// channel bit count; dwRBitMask contains the channel mask). Can be combined with DDPF_ALPHAPIXELS for a two channel
// DDS file.
DdsPixelFormat.DDPF_LUMINANCE = 0x20000;
// ---- /end VALUES USED IN dwFlags ----

DdsPixelFormat.read = function (data) { // Int32Array
	var format = new DdsPixelFormat();
	format.dwSize = data[DdsPixelFormat.HEADER_OFFSET + 0];
	if (format.dwSize !== 32) {
		throw new Error('invalid pixel format size: ' + format.dwSize);
	}
	format.dwFlags = data[DdsPixelFormat.HEADER_OFFSET + 1];
	format.dwFourCC = data[DdsPixelFormat.HEADER_OFFSET + 2];
	format.dwRGBBitCount = data[DdsPixelFormat.HEADER_OFFSET + 3];
	format.dwRBitMask = data[DdsPixelFormat.HEADER_OFFSET + 4];
	format.dwGBitMask = data[DdsPixelFormat.HEADER_OFFSET + 5];
	format.dwBBitMask = data[DdsPixelFormat.HEADER_OFFSET + 6];
	format.dwABitMask = data[DdsPixelFormat.HEADER_OFFSET + 7];
	return format;
};

function DdsHeader() {
	this.dwSize = 0;
	this.dwFlags = 0;
	this.dwHeight = 0;
	this.dwWidth = 0;
	this.dwLinearSize = 0;
	this.dwDepth = 0;
	this.dwMipMapCount = 0;
	this.dwAlphaBitDepth = 0;
	this.dwReserved1 = [];
	this.ddpf = null;
	this.dwCaps = 0;
	this.dwCaps2 = 0;
	this.dwCaps3 = 0;
	this.dwCaps4 = 0;
	this.dwTextureStage = 0;
}

// Required caps flag.
DdsHeader.DDSD_CAPS = 0x1;
// Required caps flag.
DdsHeader.DDSD_HEIGHT = 0x2;
// Required caps flag.
DdsHeader.DDSD_WIDTH = 0x4;
// Required when pitch is provided for an uncompressed texture.
DdsHeader.DDSD_PITCH = 0x8;
// Required caps flag.
DdsHeader.DDSD_PIXELFORMAT = 0x1000;
// Required in a mipmapped texture.
DdsHeader.DDSD_MIPMAPCOUNT = 0x20000;
// Required when pitch is provided for a compressed texture.
DdsHeader.DDSD_LINEARSIZE = 0x80000;
// Required in a depth texture.
DdsHeader.DDSD_DEPTH = 0x800000;
// ---- /end VALUES USED IN dwFlags ----

// ---- VALUES USED IN dwCaps ----
// Optional; must be used on any file that contains more than one surface (a mipmap, a cubic environment map, or
// volume texture).
DdsHeader.DDSCAPS_COMPLEX = 0x8;
// Optional; should be used for a mipmap.
DdsHeader.DDSCAPS_MIPMAP = 0x400000;
// Required caps flag.
DdsHeader.DDSCAPS_TEXTURE = 0x1000;
// ---- /end VALUES USED IN dwCaps ----

// ---- VALUES USED IN dwCaps2 ----
// Required for a cube map.
DdsHeader.DDSCAPS2_CUBEMAP = 0x200;
// Required when these surfaces are stored in a cube map.
DdsHeader.DDSCAPS2_CUBEMAP_POSITIVEX = 0x400;
// Required when these surfaces are stored in a cube map.
DdsHeader.DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800;
// Required when these surfaces are stored in a cube map.
DdsHeader.DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000;
// Required when these surfaces are stored in a cube map.
DdsHeader.DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000;
// Required when these surfaces are stored in a cube map.
DdsHeader.DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000;
// Required when these surfaces are stored in a cube map.
DdsHeader.DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000;
// Required for a volume texture.
DdsHeader.DDSCAPS2_VOLUME = 0x200000;
// ---- /end VALUES USED IN dwCaps2 ----

DdsHeader.read = function (data) { // Int32Array
	var header = new DdsHeader();
	header.dwSize = data[1];
	if (header.dwSize !== 124) {
		throw new Error('invalid dds header size: ' + header.dwSize);
	}
	header.dwFlags = data[2];
	header.dwHeight = data[3];
	header.dwWidth = data[4];
	header.dwLinearSize = data[5];
	header.dwDepth = data[6];
	header.dwMipMapCount = data[7];
	header.dwAlphaBitDepth = data[8];
	for (var i = 0; i < header.dwReserved1.length; i++) {
		header.dwReserved1[i] = data[9 + i];
	}
	header.ddpf = DdsPixelFormat.read(data);
	header.dwCaps = data[27];
	header.dwCaps2 = data[28];
	header.dwCaps3 = data[29];
	header.dwCaps4 = data[30];
	header.dwTextureStage = data[31];

	var expectedMipmaps = 1 + Math.ceil(Math.log(Math.max(header.dwHeight, header.dwWidth)) / Math.log(2));

	if (DdsUtils.isSet(header.dwCaps, DdsHeader.DDSCAPS_MIPMAP)) {
		if (!DdsUtils.isSet(header.dwFlags, DdsHeader.DDSD_MIPMAPCOUNT)) {
			header.dwMipMapCount = expectedMipmaps;
		} else if (header.dwMipMapCount !== expectedMipmaps) {
			console.warn('Got ' + header.dwMipMapCount + ' mipmaps, expected ' + expectedMipmaps);
		}
	} else {
		header.dwMipMapCount = 1;
	}

	return header;
};

function DdsImageInfo() {
	this.flipVertically = false;
	this.bpp = 0;
	this.header = null;
	this.headerDX10 = null;
	this.mipmapByteSizes = [];
}

DdsImageInfo.prototype.calcMipmapSizes = function (compressed) {
	var width = this.header.dwWidth;
	var height = this.header.dwHeight;
	var size = 0;

	for (var i = 0; i < this.header.dwMipMapCount; i++) {
		size = compressed ? ~~((width + 3) / 4) * ~~((height + 3) / 4) * this.bpp * 2 : ~~(width * height * this.bpp / 8);
		this.mipmapByteSizes.push(~~((size + 3) / 4) * 4);
		width = ~~(width / 2) > 1 ? ~~(width / 2) : 1;
		height = ~~(height / 2) > 1 ? ~~(height / 2) : 1;
	}
};

/**
 * Loads dds format images into a format usable by Goo.
 * @private
 */
function DdsLoader() {
}

DdsLoader.updateDepth = function (image, info) {
	if (DdsUtils.isSet(info.header.dwCaps2, DdsHeader.DDSCAPS2_CUBEMAP)) {
		var depth = 0;
		if (DdsUtils.isSet(info.header.dwCaps2, DdsHeader.DDSCAPS2_CUBEMAP_POSITIVEX)) {
			depth++;
		}
		if (DdsUtils.isSet(info.header.dwCaps2, DdsHeader.DDSCAPS2_CUBEMAP_NEGATIVEX)) {
			depth++;
		}
		if (DdsUtils.isSet(info.header.dwCaps2, DdsHeader.DDSCAPS2_CUBEMAP_POSITIVEY)) {
			depth++;
		}
		if (DdsUtils.isSet(info.header.dwCaps2, DdsHeader.DDSCAPS2_CUBEMAP_NEGATIVEY)) {
			depth++;
		}
		if (DdsUtils.isSet(info.header.dwCaps2, DdsHeader.DDSCAPS2_CUBEMAP_POSITIVEZ)) {
			depth++;
		}
		if (DdsUtils.isSet(info.header.dwCaps2, DdsHeader.DDSCAPS2_CUBEMAP_NEGATIVEZ)) {
			depth++;
		}

		if (depth !== 6) {
			throw new Error('Cubemaps without all faces defined are not currently supported.');
		}

		image.depth = depth;
	} else {
		// make sure we have at least depth of 1.
		image.depth = (info.header.dwDepth > 0 ? info.header.dwDepth : 1);
	}
};

DdsLoader.readDXT = function (imgData, totalSize, info, texture) {
	texture.image.isCompressed = true;

	if (!info.flipVertically) {
		return new Uint8Array(imgData.buffer, imgData.byteOffset + 0, totalSize);
	}

	// NB: since UNPACK_FLIP_Y_WEBGL doesn't handle compressed textures, we have to do it manually.
	var mipWidth = info.header.dwWidth;
	var mipHeight = info.header.dwHeight;

	// flip!
	var rVal = new Uint8Array(totalSize);
	var offset = 0;
	for (var mip = 0; mip < info.header.dwMipMapCount; mip++) {
		var data = imgData.subarray(offset, offset + info.mipmapByteSizes[mip]);

		var flipped = DdsUtils.flipDXT(data, mipWidth, mipHeight, texture.format);
		rVal.set(flipped, offset);
		offset += flipped.length;

		mipWidth = ~~(mipWidth / 2) > 1 ? ~~(mipWidth / 2) : 1;
		mipHeight = ~~(mipHeight / 2) > 1 ? ~~(mipHeight / 2) : 1;
	}
	return rVal;
};

DdsLoader.readUncompressed = function (imgData, totalSize, useRgb, useLum, useAlpha, useAlphaPixels, info, texture) {
	var redLumShift = DdsUtils.shiftCount(info.header.ddpf.dwRBitMask);
	var greenShift = DdsUtils.shiftCount(info.header.ddpf.dwGBitMask);
	var blueShift = DdsUtils.shiftCount(info.header.ddpf.dwBBitMask);
	var alphaShift = DdsUtils.shiftCount(info.header.ddpf.dwABitMask);

	var sourcebytesPP = ~~(info.header.ddpf.dwRGBBitCount / 8);
	var targetBytesPP = DdsUtils.getComponents(texture.format) * 1; // 1 byte per unsignedbyte store

	var rVal = new Uint8Array(totalSize);

	var mipWidth = info.header.dwWidth;
	var mipHeight = info.header.dwHeight;
	var dstOffset = 0, srcOffset = 0;
	var i = 0;
	var b = [];
	for (i = 0; i < sourcebytesPP; i++) {
		b.push(0);
	}

	for (var mip = 0; mip < info.header.dwMipMapCount; mip++) {
		for (var y = 0; y < mipHeight; y++) {
			for (var x = 0; x < mipWidth; x++) {
				for (i = 0; i < sourcebytesPP; i++) {
					b[i] = imgData[srcOffset++];
				}

				i = DdsUtils.getIntFromBytes(b);

				var redLum = ((i & info.header.ddpf.dwRBitMask) >> redLumShift);
				var green = ((i & info.header.ddpf.dwGBitMask) >> greenShift);
				var blue = ((i & info.header.ddpf.dwBBitMask) >> blueShift);
				var alpha = ((i & info.header.ddpf.dwABitMask) >> alphaShift);

				// Uncompressed, so handled by UNPACK_FLIP_Y_WEBGL instead.
				// if (info.flipVertically) {
				// dstOffset = ((mipHeight - y - 1) * mipWidth + x) * targetBytesPP;
				// }

				if (useAlpha) {
					rVal[dstOffset++] = alpha;
				} else if (useLum) {
					rVal[dstOffset++] = redLum;
					if (useAlphaPixels) {
						rVal[dstOffset++] = alpha;
					}
				} else if (useRgb) {
					rVal[dstOffset++] = redLum;
					rVal[dstOffset++] = green;
					rVal[dstOffset++] = blue;
					if (useAlphaPixels) {
						rVal[dstOffset++] = alpha;
					}
				}
			}
		}

		dstOffset += mipWidth * mipHeight * targetBytesPP;

		mipWidth = ~~(mipWidth / 2) > 1 ? ~~(mipWidth / 2) : 1;
		mipHeight = ~~(mipHeight / 2) > 1 ? ~~(mipHeight / 2) : 1;
	}

	return rVal;
};

DdsLoader.populate = function (texture, info, data) {
	var flags = info.header.ddpf.dwFlags;

	var compressedFormat = DdsUtils.isSet(flags, DdsPixelFormat.DDPF_FOURCC);
	var rgb = DdsUtils.isSet(flags, DdsPixelFormat.DDPF_RGB);
	var alphaPixels = DdsUtils.isSet(flags, DdsPixelFormat.DDPF_ALPHAPIXELS);
	var lum = DdsUtils.isSet(flags, DdsPixelFormat.DDPF_LUMINANCE);
	var alpha = DdsUtils.isSet(flags, DdsPixelFormat.DDPF_ALPHA);
	texture.type = 'UnsignedByte';

	if (compressedFormat) {
		var fourCC = info.header.ddpf.dwFourCC;
		// DXT1 format
		if (fourCC === DdsUtils.getIntFromString('DXT1')) {
			info.bpp = 4;
			// if (isSet(flags, DdsPixelFormat.DDPF_ALPHAPIXELS)) {
			// XXX: many authoring tools do not set alphapixels, so we'll error on the side of alpha
//				console.info('DDS format: DXT1A');
			texture.format = 'PrecompressedDXT1A';
			// } else {
			// logger.finest('DDS format: DXT1');
			// texture.setDataFormat(ImageDataFormat.PrecompressedDXT1);
			// }
		}

		// DXT3 format
		else if (fourCC === DdsUtils.getIntFromString('DXT3')) {
//				console.info('DDS format: DXT3');
			info.bpp = 8;
			texture.format = 'PrecompressedDXT3';
		}

		// DXT5 format
		else if (fourCC === DdsUtils.getIntFromString('DXT5')) {
//				console.info('DDS format: DXT5');
			info.bpp = 8;
			texture.format = 'PrecompressedDXT5';
		}

		// DXT10 info present...
		else if (fourCC === DdsUtils.getIntFromString('DX10')) {
			// switch (info.headerDX10.dxgiFormat) {
			// case DXGI_FORMAT_BC4_UNORM:
			// console.info('DXGI format: BC4_UNORM');
			// info.bpp = 4;
			// texture.setDataFormat(ImageDataFormat.PrecompressedLATC_L);
			// break;
			// case DXGI_FORMAT_BC5_UNORM:
			// console.info('DXGI format: BC5_UNORM');
			// info.bpp = 8;
			// texture.setDataFormat(ImageDataFormat.PrecompressedLATC_LA);
			// break;
			// default:
			// throw new Error('dxgiFormat not supported: ' + info.headerDX10.dxgiFormat);
			// }
			throw new Error('dxt10 LATC formats not supported currently: ' + info.headerDX10.dxgiFormat);
		}

		// DXT2 format - unsupported
		else if (fourCC === DdsUtils.getIntFromString('DXT2')) {
			throw new Error('DXT2 is not supported.');
		}

		// DXT4 format - unsupported
		else if (fourCC === DdsUtils.getIntFromString('DXT4')) {
			throw new Error('DXT4 is not supported.');
		}

		// Unsupported compressed type.
		else {
			throw new Error('unsupported compressed dds format found (' + fourCC + ')');
		}
	}

	// not a compressed format
	else {
		// TODO: more use of bit masks?
		// TODO: Use bit size instead of hardcoded 8 bytes? (need to also implement in readUncompressed)

		info.bpp = info.header.ddpf.dwRGBBitCount;

		// One of the RGB formats?
		if (rgb) {
			if (alphaPixels) {
//					console.info('DDS format: uncompressed rgba');
				texture.format = 'RGBA';
			} else {
//					console.info('DDS format: uncompressed rgb ');
				texture.format = 'RGB';
			}
		}

		// A luminance or alpha format
		else if (lum || alphaPixels) {
			if (lum && alphaPixels) {
//					console.info('DDS format: uncompressed LumAlpha');
				texture.format = 'LuminanceAlpha';
			}

			else if (lum) {
//					console.info('DDS format: uncompressed Lum');
				texture.format = 'Luminance';
			}

			else if (alpha) {
//					console.info('DDS format: uncompressed Alpha');
				texture.format = 'Alpha';
			}
		} // end luminance/alpha type

		// Unsupported type.
		else {
			throw new Error('unsupported uncompressed dds format found.');
		}
	}

	info.calcMipmapSizes(compressedFormat);
	texture.image.mipmapSizes = (info.mipmapByteSizes);

	// Add up total byte size of single depth layer
	var totalSize = 0;
	for (var i = 0; i < info.mipmapByteSizes.length; i++) {
		totalSize += info.mipmapByteSizes[i];
	}

	// Go through and load in image data
	var imageData = [];
	for (var i = 0; i < texture.image.depth; i++) {
		// read in compressed data
		if (compressedFormat) {
			imageData.push(DdsLoader.readDXT(data, totalSize, info, texture));
		}

		// read in uncompressed data
		else if (rgb || lum || alpha) {
			imageData.push(DdsLoader.readUncompressed(data, totalSize, rgb, lum, alpha, alphaPixels, info, texture));
		}
	}

	// set on image
	texture.image.data = texture.image.depth === 1 ? imageData[0] : imageData;
	texture.image.useArrays = true;
};

DdsLoader.prototype.load = function (buffer, tex, flipped, arrayByteOffset, arrayByteLength) {
	var header = new Int32Array(buffer, arrayByteOffset + 0, 32);

	// Read and check magic word...
	var dwMagic = header[0];
	if (dwMagic !== DdsUtils.getIntFromString('DDS ')) {
		throw new Error('Not a dds file.');
	}
//		console.info('Reading DDS file.');

	// Create our data store;
	var info = new DdsImageInfo();

	info.flipVertically = flipped;

	// Read standard dds header
	info.header = DdsHeader.read(header);

	// if applicable, read DX10 header
	info.headerDX10 = info.header.ddpf.dwFourCC === DdsUtils.getIntFromString('DX10') ? DdsHeader.read(Int32Array.create(buffer,
		arrayByteOffset + 128, 5)) : null;

	// Create our new image
	var image = tex.image;
	if (typeof image === 'undefined' || image === null) {
		image = {};
		tex.image = image;
	}
	image.width = info.header.dwWidth;
	image.height = info.header.dwHeight;

	// update depth based on flags / header
	DdsLoader.updateDepth(image, info);

	// add our format and image data.
	var contentOffset = 128 + (info.headerDX10 ? 20 : 0);
	DdsLoader.populate(tex, info, new Uint8Array(buffer, arrayByteOffset + contentOffset, arrayByteLength - contentOffset));

	if (!info.mipmapByteSizes || info.mipmapByteSizes.length < 2) {
		tex.minFilter = 'BilinearNoMipMaps';
	}

	image.bpp = info.bpp;
	image.dataReady = true;
	image.isData = true;
	tex.needsUpdate = true;
};

DdsLoader.prototype.isSupported = function () {
	return !!Capabilities.CompressedTextureS3TC;
};

DdsLoader.prototype.toString = function () {
	return 'DdsLoader';
};

module.exports = DdsLoader;